// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo

/* ReportEngine.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using AM.Reporting.Data;
using AM.Reporting.Preview;
using AM.Reporting.Utils;

using System;
using System.Collections.Generic;

#endregion

#nullable enable

namespace AM.Reporting.Engine;

/// <summary>
/// Represents the report engine.
/// </summary>
public partial class ReportEngine
{
    #region Properties

    private Report Report { get; }

    private PreparedPages PreparedPages => Report.PreparedPages.ThrowIfNull();

    /// <summary>
    /// Gets or sets the current X offset.
    /// </summary>
    /// <remarks>
    /// This property specifies the X offset where the current band will be printed.
    /// </remarks>
    public float CurX
    {
        get => _curX;
        set => _curX = Converter.DecreasePrecision (value, 2);
    }

    /// <summary>
    /// Gets or sets the current Y offset.
    /// </summary>
    /// <remarks>
    /// This property specifies the Y offset where the current band will be printed.
    /// After the band is printed, this value is incremented by the band's height.
    /// </remarks>
    public float CurY
    {
        get => _curY;
        set => _curY = Converter.DecreasePrecision (value, 2);
    }

    /// <summary>
    /// Gets the index of currently printing column in the multi-column report.
    /// </summary>
    /// <remarks>
    /// This value is 0-based.
    /// </remarks>
    public int CurColumn { get; private set; }

    /// <summary>
    /// Gets or sets index of current prepared page the current band will print on.
    /// </summary>
    /// <remarks>
    /// Note: the page with specified index must exists. This property is used to print side-by-side
    /// subreports and Table object. Usually you don't need to use it.
    /// </remarks>
    public int CurPage
    {
        get => PreparedPages.CurPage;
        set => PreparedPages.CurPage = value;
    }

    /// <summary>
    /// Gets the current page width, in pixels.
    /// </summary>
    /// <remarks>
    /// This property returns a paper width minus left and right margins.
    /// </remarks>
    public float PageWidth => _page.WidthInPixels - (_page.LeftMargin + _page.RightMargin) * Units.Millimeters;

    /// <summary>
    /// Gets the current page height, in pixels.
    /// </summary>
    /// <remarks>
    /// This property returns a paper height minus top and bottom margins.
    /// </remarks>
    public float PageHeight => _page.HeightInPixels - (_page.TopMargin + _page.BottomMargin) * Units.Millimeters;

    /// <summary>
    /// Gets the value indicating whether the page has unlimited height.
    /// </summary>
    public bool UnlimitedHeight => _page.UnlimitedHeight;

    /// <summary>
    /// Gets the value indicating whether the page has unlimited width.
    /// </summary>
    public bool UnlimitedWidth => _page.UnlimitedWidth;

    /// <summary>
    /// Gets or sets the current height of unlimited page.
    /// </summary>
    public float UnlimitedHeightValue
    {
        get => _page.UnlimitedHeightValue;
        set => _page.UnlimitedHeightValue = value;
    }

    /// <summary>
    /// Gets or sets the current width of unlimited page.
    /// </summary>
    public float UnlimitedWidthValue
    {
        get => _page.UnlimitedWidthValue;
        set => _page.UnlimitedWidthValue = value;
    }

    /// <summary>
    /// Gets the height of page footer (including all its child bands), in pixels.
    /// </summary>
    public float PageFooterHeight => GetBandHeightWithChildren (_page.PageFooter);

    /// <summary>
    /// Gets the height of column footer (including all its child bands), in pixels.
    /// </summary>
    public float ColumnFooterHeight => GetBandHeightWithChildren (_page.ColumnFooter);

    /// <summary>
    /// Gets the free space on the current page, in pixels.
    /// </summary>
    /// <remarks>
    /// This property returns the page height minus footers height minus <b>CurY</b> value.
    /// </remarks>
    public float FreeSpace
    {
        get
        {
            var pageHeight = PageHeight;
            pageHeight -= PageFooterHeight;
            pageHeight -= ColumnFooterHeight;
            pageHeight -= GetFootersHeight();
            return Converter.DecreasePrecision (pageHeight - CurY, 2);
        }
    }

    /// <summary>
    /// Gets the current prepared page number.
    /// </summary>
    /// <remarks>
    /// This value is 1-based. The initial value (usually 1) is set in the Report.InitialPageNumber property.
    /// </remarks>
    public int PageNo => GetLogicalPageNumber();

    /// <summary>
    /// Gets the number of total pages in a prepared report.
    /// </summary>
    /// <remarks>
    /// To use this property, your report must be two-pass. Set the <see cref="AM.Reporting.Report.DoublePass"/>
    /// property to <b>true</b>.
    /// </remarks>
    public int TotalPages => GetLogicalTotalPages();

    /// <summary>
    /// Gets the string that represents the current page number.
    /// </summary>
    /// <remarks>
    /// This property returns a locale-based value, for example: "Page 1".
    /// </remarks>
    public string PageN => string.Format (Res.Get ("Misc,PageN"), PageNo);

    /// <summary>
    /// Gets the string that represents the "Page N of M" number.
    /// </summary>
    /// <remarks>
    /// This property returns a locale-based value, for example: "Page 1 of 10".
    /// </remarks>
    public string PageNofM => string.Format (Res.Get ("Misc,PageNofM"), PageNo, TotalPages);

    /// <summary>
    /// Gets the current row number of currently printing band.
    /// </summary>
    /// <remarks>
    /// This value is 1-based. It resets to 1 on each new group.
    /// </remarks>
    public int RowNo => _curBand == null ? 0 : _curBand.RowNo;

    /// <summary>
    /// Gets the running current row number of currently printing band.
    /// </summary>
    /// <remarks>
    /// This value is 1-based.
    /// </remarks>
    public int AbsRowNo => _curBand == null ? 0 : _curBand.AbsRowNo;

    /// <summary>
    /// Gets the date of report start.
    /// </summary>
    public DateTime Date { get; private set; }

    /// <summary>
    /// Gets a value indicating whether the report is executing the final pass.
    /// </summary>
    /// <remarks>
    /// This property is <b>true</b> if report is one-pass, or if report is two-pass and
    /// the second pass is executing.
    /// </remarks>
    public bool FinalPass { get; private set; }

    /// <summary>
    /// Gets a value indicating whether the report is executing the first pass.
    /// </summary>
    /// <remarks>
    /// This property is <b>true</b> if report is one-pass, or if report is two-pass and
    /// the first pass is executing.
    /// </remarks>
    public bool FirstPass => !(Report.DoublePass && FinalPass);

    /// <summary>
    /// Gets a level of hierarchy when printing hierarchical bands.
    /// </summary>
    /// <remarks>
    /// The first level of hierarchy has 0 index.
    /// </remarks>
    public int HierarchyLevel { get; private set; }

    /// <summary>
    /// Gets the row number like "1.2.1" when printing hierarchical bands.
    /// </summary>
    public string HierarchyRowNo { get; private set; }

    #endregion

    #region Constructors

    internal ReportEngine (Report report)
    {
        Report = report;
        _objectsToProcess = new List<ProcessInfo>();
    }

    #endregion Constructors

    #region Private members

    private float _curX;
    private float _originX;
    private float _curY;
    private BandBase? _curBand;
    private float _hierarchyIndent;
    private int _firstReportPage;
    private bool _isFirstReportPage;
    private int _pagesLimit;

    private void ResetDesigningFlag()
    {
        var allObjects = Report.AllObjects;
        foreach (Base c in allObjects)
        {
            c.SetDesigning (false);
        }
    }

    private void InitializeData()
    {
        foreach (Base c in Report.Dictionary.AllObjects)
        {
            if (c is DataComponentBase @base)
            {
                @base.InitializeComponent();
            }
        }

        foreach (Base c in Report.AllObjects)
        {
            if (c is ReportComponentBase obj)
            {
                obj.ResetData();
            }
        }
    }

    private void InitializeSecondPassData()
    {
        foreach (Base c in Report.Dictionary.AllObjects)
        {
            if (c is DataSourceBase { RowCount: > 0 } data)
            {
                data.First();
            }
        }
    }

    private void PrepareToFirstPass (bool append)
    {
        FinalPass = !Report.DoublePass;
        if (!append)
        {
            PreparedPages.Clear();
        }
        else
        {
            PreparedPages.CurPage = PreparedPages.Count > 0 ? PreparedPages.Count - 1 : 0;
        }

        _isFirstReportPage = true;
        HierarchyLevel = 1;
        PreparedPages.PrepareToFirstPass();
        Report.Dictionary.Totals.ClearValues();
        _objectsToProcess.Clear();
        InitializePages();
        InitPageNumbers();
    }

    private void PrepareToSecondPass()
    {
        Report.Dictionary.Totals.ClearValues();
        _objectsToProcess.Clear();
        PreparedPages.ClearFirstPass();
        InitializeSecondPassData();
    }

    private float GetBandHeightWithChildren (BandBase band)
    {
        float result = 0;

        while (band != null)
        {
            if (CanPrint (band))
            {
                result += (band.CanGrow || band.CanShrink) ? CalcHeight (band) : band.Height;
            }
            else if (FinalPass && !string.IsNullOrEmpty (band.VisibleExpression) &&
                     band.VisibleExpression.Contains ("TotalPages"))
            {
                result += (band.CanGrow || band.CanShrink) ? CalcHeight (band) : band.Height;
            }

            band = band.Child;
            if (band != null && ((band as ChildBand).FillUnusedSpace || (band as ChildBand).CompleteToNRows != 0))
            {
                break;
            }
        }

        return result;
    }

    private void RunReportPages (ReportPage page)
    {
        OnStateChanged (Report, EngineState.ReportStarted);

        if (page == null)
        {
            RunReportPages();
        }
        else
        {
            RunReportPage (page);
        }

        OnStateChanged (Report, EngineState.ReportFinished);
    }

    #endregion

    #region Internal methods

    internal bool Run
        (
            bool runDialogs,
            bool append,
            bool resetDataState
        )
    {
        return Run (runDialogs, append, resetDataState, null);
    }

    internal bool Run
        (
            bool runDialogs,
            bool append,
            bool resetDataState,
            int pagesLimit
        )
    {
        _pagesLimit = pagesLimit;
        return Run (runDialogs, append, resetDataState, null);
    }

    internal bool Run
        (
            bool runDialogs,
            bool append,
            bool resetDataState,
            ReportPage? page
        )
    {
        RunPhase1 (resetDataState);

        try
        {
            if (runDialogs && !RunDialogs())
            {
                return false;
            }

            Config.ReportSettings.OnStartProgress (Report);
            PrepareToFirstPass (append);
            RunReportPages (page);

            ResetLogicalPageNumber();
            if (Report is { DoublePass: true, Aborted: false })
            {
                FinalPass = true;
                PrepareToSecondPass();
                RunReportPages (page);
            }
        }
        finally
        {
            Report.OnFinishReport (EventArgs.Empty);
            Config.ReportSettings.OnFinishProgress (Report);
            Report.SetOperation (ReportOperation.None);

            // limit the prepared pages
            if (Report.MaxPages > 0)
            {
                while (PreparedPages.Count > Report.MaxPages)
                {
                    PreparedPages.RemovePage (PreparedPages.Count - 1);
                }
            }

            // Limit the prepared pages again because pagesLimit has better priority than Report.MaxPages.
            if (_pagesLimit > 0)
            {
                while (PreparedPages.Count > _pagesLimit)
                {
                    PreparedPages.RemovePage (PreparedPages.Count - 1);
                }
            }
        }

        return true;
    }

    internal void RunPhase1 (bool resetDataState = true, bool webDialog = false)
    {
        Date = SystemFake.DateTime.Now;
        Report.SetOperation (ReportOperation.Running);
        ResetDesigningFlag();

        // don't reset the data state if we run the hyperlink's detail page or refresh a report.
        // This is necessary to keep data filtering settings alive
        if (resetDataState)
        {
            InitializeData();
        }

        // don't call OnStartReport event again, if it's web dialog re-render
        if (!webDialog)
        {
            Report.OnStartReport (EventArgs.Empty);
        }
    }

    internal void RunPhase2 (int? pagesLimit = null)
    {
        if (pagesLimit != null)
        {
            this._pagesLimit = pagesLimit.Value;
        }

        try
        {
            Config.ReportSettings.OnStartProgress (Report);
            PrepareToFirstPass (false);
            RunReportPages();

            ResetLogicalPageNumber();
            if (Report is { DoublePass: true, Aborted: false })
            {
                FinalPass = true;
                PrepareToSecondPass();
                RunReportPages();
            }
        }
        finally
        {
            Report.OnFinishReport (EventArgs.Empty);
            Config.ReportSettings.OnFinishProgress (Report);
            Report.SetOperation (ReportOperation.None);

            // limit the prepared pages
            if (Report.MaxPages > 0)
            {
                while (PreparedPages.Count > Report.MaxPages)
                {
                    PreparedPages.RemovePage (PreparedPages.Count - 1);
                }
            }

            // Limit the prepared pages again because pagesLimit has better priority than Report.MaxPages.
            if (pagesLimit > 0)
            {
                while (PreparedPages.Count > pagesLimit)
                {
                    PreparedPages.RemovePage (PreparedPages.Count - 1);
                }
            }
        }
    }

    #endregion
}
